import {
  defaultAffectedJSObjects,
  evalAndLintingHandler,
  evalQueueBuffer,
  evaluateTreeSaga,
  evaluationLoopWithDebounce,
  type BUFFERED_ACTION,
} from "./EvaluationsSaga";
import { evalWorker } from "utils/workerInstances";
import { expectSaga, testSaga } from "redux-saga-test-plan";

import { EVAL_WORKER_ACTIONS } from "ee/workers/Evaluation/evalWorkerActions";
import { select } from "redux-saga/effects";
import { getMetaWidgets, getWidgets, getWidgetsMeta } from "./selectors";
import {
  getAllActionValidationConfig,
  getCurrentPageDSLVersion,
} from "ee//selectors/entitiesSelector";
import { getSelectedAppTheme } from "selectors/appThemingSelectors";
import { getAppMode } from "ee/selectors/applicationSelectors";
import * as log from "loglevel";

import type { ReduxAction } from "actions/ReduxActionTypes";
import {
  ReduxActionErrorTypes,
  ReduxActionTypes,
} from "ee/constants/ReduxActionConstants";
import { fetchPluginFormConfigsSuccess } from "actions/pluginActions";
import { createJSCollectionSuccess } from "actions/jsActionActions";
import { getInstanceId } from "ee/selectors/organizationSelectors";
import {
  getApplicationLastDeployedAt,
  getCurrentApplicationId,
  getCurrentPageId,
} from "selectors/editorSelectors";
import { updateActionData } from "actions/pluginActionActions";
import watchInitSagas from "./InitSagas";

import { clearAllWidgetFactoryCache } from "WidgetProvider/factory/decorators";

jest.mock("loglevel");
jest.mock("utils/editor/EditorUtils", () => ({
  registerAllWidgets: jest.fn(),
}));
jest.mock("WidgetProvider/factory/decorators", () => ({
  clearAllWidgetFactoryCache: jest.fn(),
}));

describe("evaluateTreeSaga", () => {
  afterAll(() => {
    jest.unmock("loglevel");
  });
  test("should set 'shouldRespondWithLogs'to evaluations when the log level is debug", async () => {
    // TODO: Fix this the next time the file is edited
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (log.getLevel as any).mockReturnValue(log.levels.DEBUG);
    const unEvalAndConfigTree = { unEvalTree: {}, configTree: {} };

    return expectSaga(evaluateTreeSaga, unEvalAndConfigTree)
      .provide([
        [select(getAllActionValidationConfig), {}],
        [select(getWidgets), {}],
        [select(getMetaWidgets), {}],
        [select(getSelectedAppTheme), {}],
        [select(getAppMode), false],
        [select(getWidgetsMeta), {}],
        [select(getInstanceId), "instanceId"],
        [select(getCurrentApplicationId), "applicationId"],
        [select(getCurrentPageId), "pageId"],
        [
          select(getApplicationLastDeployedAt),
          new Date("11 September 2024").toISOString(),
        ],
        [select(getCurrentPageDSLVersion), 1],
      ])
      .call(
        evalWorker.request,
        EVAL_WORKER_ACTIONS.EVAL_TREE,
        {
          cacheProps: {
            instanceId: "instanceId",
            appId: "applicationId",
            pageId: "pageId",
            appMode: false,
            timestamp: new Date("11 September 2024").toISOString(),
            dslVersion: 1,
          },
          unevalTree: unEvalAndConfigTree,
          widgetTypeConfigMap: undefined,
          widgets: {},
          theme: {},
          shouldReplay: true,
          allActionValidationConfig: {},
          forceEvaluation: false,
          metaWidgets: {},
          appMode: false,
          widgetsMeta: {},
          shouldRespondWithLogs: true,
          affectedJSObjects: { ids: [], isAllAffected: false },
          actionDataPayloadConsolidated: undefined,
        },
        false,
      )
      .run();
  });
  test("should set 'shouldRespondWithLogs' to false when the log level is not debug", async () => {
    // TODO: Fix this the next time the file is edited
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (log.getLevel as any).mockReturnValue(log.levels.INFO);
    const unEvalAndConfigTree = { unEvalTree: {}, configTree: {} };

    return expectSaga(evaluateTreeSaga, unEvalAndConfigTree)
      .provide([
        [select(getAllActionValidationConfig), {}],
        [select(getWidgets), {}],
        [select(getMetaWidgets), {}],
        [select(getSelectedAppTheme), {}],
        [select(getAppMode), false],
        [select(getWidgetsMeta), {}],
        [select(getInstanceId), "instanceId"],
        [select(getCurrentApplicationId), "applicationId"],
        [select(getCurrentPageId), "pageId"],
        [
          select(getApplicationLastDeployedAt),
          new Date("11 September 2024").toISOString(),
        ],
        [select(getCurrentPageDSLVersion), 1],
      ])
      .call(
        evalWorker.request,
        EVAL_WORKER_ACTIONS.EVAL_TREE,
        {
          cacheProps: {
            instanceId: "instanceId",
            appId: "applicationId",
            pageId: "pageId",
            appMode: false,
            timestamp: new Date("11 September 2024").toISOString(),
            dslVersion: 1,
          },
          unevalTree: unEvalAndConfigTree,
          widgetTypeConfigMap: undefined,
          widgets: {},
          theme: {},
          shouldReplay: true,
          allActionValidationConfig: {},
          forceEvaluation: false,
          metaWidgets: {},
          appMode: false,
          widgetsMeta: {},
          shouldRespondWithLogs: false,
          affectedJSObjects: { ids: [], isAllAffected: false },
          actionDataPayloadConsolidated: undefined,
        },
        false,
      )
      .run();
  });
  test("should propagate affectedJSObjects property to evaluation action", async () => {
    const unEvalAndConfigTree = { unEvalTree: {}, configTree: {} };
    const affectedJSObjects = {
      isAllAffected: false,
      ids: ["1", "2"],
    };

    return expectSaga(
      evaluateTreeSaga,
      unEvalAndConfigTree,
      [],
      undefined,
      undefined,
      undefined,
      affectedJSObjects,
    )
      .provide([
        [select(getAllActionValidationConfig), {}],
        [select(getWidgets), {}],
        [select(getMetaWidgets), {}],
        [select(getSelectedAppTheme), {}],
        [select(getAppMode), false],
        [select(getWidgetsMeta), {}],
        [select(getInstanceId), "instanceId"],
        [select(getCurrentApplicationId), "applicationId"],
        [select(getCurrentPageId), "pageId"],
        [
          select(getApplicationLastDeployedAt),
          new Date("11 September 2024").toISOString(),
        ],
        [select(getCurrentPageDSLVersion), 1],
      ])
      .call(
        evalWorker.request,
        EVAL_WORKER_ACTIONS.EVAL_TREE,
        {
          cacheProps: {
            instanceId: "instanceId",
            appId: "applicationId",
            pageId: "pageId",
            appMode: false,
            timestamp: new Date("11 September 2024").toISOString(),
            dslVersion: 1,
          },
          unevalTree: unEvalAndConfigTree,
          widgetTypeConfigMap: undefined,
          widgets: {},
          theme: {},
          shouldReplay: true,
          allActionValidationConfig: {},
          forceEvaluation: false,
          metaWidgets: {},
          appMode: false,
          widgetsMeta: {},
          shouldRespondWithLogs: false,
          affectedJSObjects,
          actionDataPayloadConsolidated: undefined,
        },
        false,
      )
      .run();
  });
  test("should call evalWorker.request with isFirstEvaluation as true when isFirstEvaluation is set as true in evaluateTreeSaga", async () => {
    const unEvalAndConfigTree = { unEvalTree: {}, configTree: {} };
    const isFirstEvaluation = true;

    return expectSaga(
      evaluateTreeSaga,
      unEvalAndConfigTree,
      [],
      undefined,
      undefined,
      undefined,
      undefined,
      undefined,
      isFirstEvaluation,
    )
      .provide([
        [select(getAllActionValidationConfig), {}],
        [select(getWidgets), {}],
        [select(getMetaWidgets), {}],
        [select(getSelectedAppTheme), {}],
        [select(getAppMode), false],
        [select(getWidgetsMeta), {}],
        [select(getInstanceId), "instanceId"],
        [select(getCurrentApplicationId), "applicationId"],
        [select(getCurrentPageId), "pageId"],
        [
          select(getApplicationLastDeployedAt),
          new Date("11 September 2024").toISOString(),
        ],
        [select(getCurrentPageDSLVersion), 1],
      ])
      .call(
        evalWorker.request,
        EVAL_WORKER_ACTIONS.EVAL_TREE,
        {
          cacheProps: {
            instanceId: "instanceId",
            appId: "applicationId",
            pageId: "pageId",
            appMode: false,
            timestamp: new Date("11 September 2024").toISOString(),
            dslVersion: 1,
          },
          unevalTree: unEvalAndConfigTree,
          widgetTypeConfigMap: undefined,
          widgets: {},
          theme: {},
          shouldReplay: true,
          allActionValidationConfig: {},
          forceEvaluation: false,
          metaWidgets: {},
          appMode: false,
          widgetsMeta: {},
          shouldRespondWithLogs: false,
          affectedJSObjects: { ids: [], isAllAffected: false },
          actionDataPayloadConsolidated: undefined,
        },
        true,
      )
      .run();
  });
});

describe("evalQueueBuffer", () => {
  test("should return a buffered action with the default affectedJSObjects state for an action which does not have affectedJSObjects associated to it", () => {
    const buffer = evalQueueBuffer();

    // this action does not generate an affectedJSObject
    // TODO: Fix this the next time the file is edited
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    buffer.put(fetchPluginFormConfigsSuccess({} as any));
    const bufferedAction = buffer.take();

    expect(bufferedAction).toEqual({
      actionDataPayloadConsolidated: [],
      hasBufferedAction: true,
      hasDebouncedHandleUpdate: false,
      type: ReduxActionTypes.BUFFERED_ACTION,
      affectedJSObjects: defaultAffectedJSObjects,
      postEvalActions: [],
    });
  });
  test("should club all JS actions affectedJSObjects's ids", () => {
    const buffer = evalQueueBuffer();

    // TODO: Fix this the next time the file is edited
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    buffer.put(createJSCollectionSuccess({ id: "1" } as any));
    // TODO: Fix this the next time the file is edited
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    buffer.put(createJSCollectionSuccess({ id: "2" } as any));
    const bufferedAction = buffer.take();

    expect(bufferedAction).toEqual({
      actionDataPayloadConsolidated: [],
      hasBufferedAction: true,
      hasDebouncedHandleUpdate: false,
      type: ReduxActionTypes.BUFFERED_ACTION,
      affectedJSObjects: { ids: ["1", "2"], isAllAffected: false },
      postEvalActions: [],
    });
  });
  test("should return all JS actions that have changed when there is a pending action which affects all JS actions ", () => {
    const buffer = evalQueueBuffer();

    // TODO: Fix this the next time the file is edited
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    buffer.put(createJSCollectionSuccess({ id: "1" } as any));
    // this action triggers an isAllAffected flag
    buffer.put({
      type: ReduxActionErrorTypes.FETCH_JS_ACTIONS_ERROR,
    } as ReduxAction<unknown>);
    // queue is not empty
    expect(buffer.isEmpty()).not.toBeTruthy();

    const bufferedAction = buffer.take();

    expect(bufferedAction).toEqual({
      actionDataPayloadConsolidated: [],
      hasBufferedAction: true,
      hasDebouncedHandleUpdate: false,
      type: ReduxActionTypes.BUFFERED_ACTION,
      affectedJSObjects: { ids: [], isAllAffected: true },
      postEvalActions: [],
    });
    expect(buffer.isEmpty()).toBeTruthy();
  });
  test("should reset the collectedAffectedJSObjects after the buffered action has been dequeued and the subsequent actions should have the defaultAffectedJSObjects", () => {
    const buffer = evalQueueBuffer();

    // TODO: Fix this the next time the file is edited
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    buffer.put(createJSCollectionSuccess({ id: "1" } as any));
    const bufferedAction = buffer.take();

    expect(bufferedAction).toEqual({
      actionDataPayloadConsolidated: [],
      hasBufferedAction: true,
      hasDebouncedHandleUpdate: false,
      type: ReduxActionTypes.BUFFERED_ACTION,
      affectedJSObjects: { ids: ["1"], isAllAffected: false },
      postEvalActions: [],
    });
    expect(buffer.isEmpty()).toBeTruthy();
    // this action does not generate an affectedJSObject, So the subsequent buffered action should have default affectedJSObjects
    // TODO: Fix this the next time the file is edited
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    buffer.put(fetchPluginFormConfigsSuccess({ id: "1" } as any));
    const bufferedActionsWithDefaultAffectedJSObjects = buffer.take();

    expect(bufferedActionsWithDefaultAffectedJSObjects).toEqual({
      actionDataPayloadConsolidated: [],
      hasBufferedAction: true,
      hasDebouncedHandleUpdate: false,
      type: ReduxActionTypes.BUFFERED_ACTION,
      affectedJSObjects: defaultAffectedJSObjects,
      postEvalActions: [],
    });
  });
  test("should debounce UPDATE_ACTION_DATA actions together when the buffer is busy", () => {
    const buffer = evalQueueBuffer();

    buffer.put(
      updateActionData([
        {
          entityName: "widget1",
          dataPath: "data",
          data: { a: 1 },
          dataPathRef: "",
        },
      ]),
    );
    buffer.put(
      updateActionData([
        {
          entityName: "widget2",
          dataPath: "data",
          data: { a: 2 },
          dataPathRef: "",
        },
      ]),
    );
    const bufferedActionsWithDefaultAffectedJSObjects = buffer.take();

    expect(bufferedActionsWithDefaultAffectedJSObjects).toEqual({
      actionDataPayloadConsolidated: [
        {
          data: {
            a: 1,
          },
          dataPath: "data",
          dataPathRef: "",
          entityName: "widget1",
        },
        {
          data: {
            a: 2,
          },
          dataPath: "data",
          dataPathRef: "",
          entityName: "widget2",
        },
      ],

      hasBufferedAction: false,
      hasDebouncedHandleUpdate: true,
      type: ReduxActionTypes.BUFFERED_ACTION,
      affectedJSObjects: defaultAffectedJSObjects,
      postEvalActions: [],
    });
  });
  test("should be able to debounce UPDATE_ACTION_DATA actions and BUFFERED_ACTION together when the buffer is busy", () => {
    const buffer = evalQueueBuffer();

    buffer.put(
      updateActionData([
        {
          entityName: "widget1",
          dataPath: "data",
          data: { a: 1 },
          dataPathRef: "",
        },
      ]),
    );
    buffer.put(
      updateActionData([
        {
          entityName: "widget2",
          dataPath: "data",
          data: { a: 2 },
          dataPathRef: "",
        },
      ]),
    );

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    buffer.put(createJSCollectionSuccess({ id: "1" } as any));

    const bufferedActionsWithDefaultAffectedJSObjects = buffer.take();

    expect(bufferedActionsWithDefaultAffectedJSObjects).toEqual({
      actionDataPayloadConsolidated: [
        {
          data: {
            a: 1,
          },
          dataPath: "data",
          dataPathRef: "",
          entityName: "widget1",
        },
        {
          data: {
            a: 2,
          },
          dataPath: "data",
          dataPathRef: "",
          entityName: "widget2",
        },
      ],

      hasBufferedAction: true,
      hasDebouncedHandleUpdate: true,
      type: ReduxActionTypes.BUFFERED_ACTION,
      affectedJSObjects: { ids: ["1"], isAllAffected: false },
      postEvalActions: [],
    });
  });
});

describe("evaluationLoopWithDebounce", () => {
  describe("debounce", () => {
    test("should call a regular evaluation with the consolidated action data payload when both updateActionData and evaluation action is triggered", async () => {
      const buffer = evalQueueBuffer();

      buffer.put(
        updateActionData([
          {
            entityName: "widget1",
            dataPath: "data",
            data: { a: 1 },
            dataPathRef: "",
          },
        ]),
      );
      buffer.put(
        updateActionData([
          {
            entityName: "widget2",
            dataPath: "data",
            data: { a: 2 },
            dataPathRef: "",
          },
        ]),
      );
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      buffer.put(createJSCollectionSuccess({ id: "1" } as any));
      const action = buffer.take();

      const mockChannel = "mock-channel";

      // assert that a regular evaluation is only triggered and no evalTreeWithChanges evaluation is triggered
      return (
        testSaga(evaluationLoopWithDebounce, mockChannel)
          .next()
          .take(mockChannel)
          .next(action)
          .call(evalAndLintingHandler, true, action, {
            actionDataPayloadConsolidated: [
              {
                entityName: "widget1",
                dataPath: "data",
                data: { a: 1 },
                dataPathRef: "",
              },
              {
                entityName: "widget2",
                dataPath: "data",
                data: { a: 2 },
                dataPathRef: "",
              },
            ],
            shouldReplay: undefined,
            forceEvaluation: false,
            requiresLogging: undefined,
            affectedJSObjects: { isAllAffected: false, ids: ["1"] },
          })
          .next()
          // wait for the next action in the event loop
          .take(mockChannel)
      );
    });
    test("should call an evalTreeWithChanges when only updateActionData actions are triggered", async () => {
      const buffer = evalQueueBuffer();

      buffer.put(
        updateActionData([
          {
            entityName: "widget1",
            dataPath: "data",
            data: { a: 1 },
            dataPathRef: "",
          },
        ]),
      );
      buffer.put(
        updateActionData([
          {
            entityName: "widget2",
            dataPath: "data",
            data: { a: 2 },
            dataPathRef: "",
          },
        ]),
      );
      const action = buffer.take() as unknown as BUFFERED_ACTION;

      const mockChannel = "mock-channel";

      return (
        testSaga(evaluationLoopWithDebounce, mockChannel)
          .next()
          .take(mockChannel)
          .next(action)
          .call(
            evalWorker.request,
            EVAL_WORKER_ACTIONS.UPDATE_ACTION_DATA,
            action.actionDataPayloadConsolidated,
          )
          .next()
          // wait for the next action in the event loop
          .take(mockChannel)
      );
    });
    test("should call a regular evaluation when evaluation actions are triggered", async () => {
      const buffer = evalQueueBuffer();

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      buffer.put(createJSCollectionSuccess({ id: "1" } as any));

      const action = buffer.take();

      const mockChannel = "mock-channel";

      return (
        testSaga(evaluationLoopWithDebounce, mockChannel)
          .next()
          .take(mockChannel)
          .next(action)
          .call(evalAndLintingHandler, true, action, {
            shouldReplay: undefined,
            forceEvaluation: false,
            requiresLogging: undefined,
            affectedJSObjects: { isAllAffected: false, ids: ["1"] },
          })
          .next()
          // wait for the next action in the event loop
          .take(mockChannel)
      );
    });
  });
});

describe("first evaluation integration", () => {
  it("should call clearAllWidgetFactoryCache when WIDGET_INIT_SUCCESS is dispatched", async () => {
    await expectSaga(watchInitSagas)
      .dispatch({
        type: ReduxActionTypes.WIDGET_INIT_SUCCESS,
      })
      .silentRun();

    expect(clearAllWidgetFactoryCache).toHaveBeenCalled();
  });
});
